package com.reebake.ideal.security.authentication;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.func.LambdaUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.reebake.ideal.security.core.AuthenticationFactor;
import com.reebake.ideal.security.core.AuthenticationFactorConvertor;
import com.reebake.ideal.security.properties.ServerSecurityProperties;
import com.reebake.ideal.servlet.core.ByteContentCachingRequestWrapper;
import com.reebake.ideal.servlet.util.WebUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Setter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.util.WebUtils;

import java.util.ArrayList;
import java.util.List;

@Setter
public class SmartLoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    private ObjectMapper objectMapper;
    private ServerSecurityProperties serverSecurityProperties;

    private boolean postOnly = true;

    public SmartLoginAuthenticationFilter(String defaultFilterProcessesUrl) {
        super(new AntPathRequestMatcher(defaultFilterProcessesUrl, "POST"));
    }

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        ByteContentCachingRequestWrapper requestWrapper = WebUtils.getNativeRequest(request, ByteContentCachingRequestWrapper.class);
        if(ObjUtil.isNull(requestWrapper)) {
            request = new ByteContentCachingRequestWrapper(request);
        }
        List<Authentication> authentications = extract(request);
        if (CollUtil.isEmpty(authentications)) {
            throw new AuthenticationServiceException("Can not create AuthenticationToken");
        }
        if(BooleanUtil.isTrue(serverSecurityProperties.getEnable2FA())
            && authentications.size() != 2){
            throw new BadCredentialsException("2FA authentication enabled. please check your input.");
        }
        AbstractAuthenticationToken mainAuthentication = (AbstractAuthenticationToken) authentications.get(0);
        this.setDetails(request, mainAuthentication);

        List<Authentication> authenticatedTokens = new ArrayList<>();
        for(Authentication unauthenticatedToken : authentications) {
            AbstractAuthenticationToken authentication = (AbstractAuthenticationToken) unauthenticatedToken;
            Authentication authenticatedToken = this.getAuthenticationManager().authenticate(authentication);
            authenticatedTokens.add(authenticatedToken);
        }

        return authenticatedTokens.get(0);
    }

    protected void setDetails(HttpServletRequest request, AbstractAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public List<Authentication> extract(HttpServletRequest request) {
        if (!WebUtil.isAjaxRequest(request)) {
            throw new BadCredentialsException("only support ajax json request.");
        }
        String requestBody = null;
        try {
            requestBody = IoUtil.read(request.getInputStream(), CharsetUtil.CHARSET_UTF_8);
        }catch(Exception e) {
            throw new BadCredentialsException(e.getLocalizedMessage());
        }
        if(StrUtil.isBlank(requestBody)) {
            throw new BadCredentialsException("request body is blank.");
        }
        if(!JSONUtil.isTypeJSONArray(requestBody)) {
            throw new BadCredentialsException("is not array.");
        }
        JsonNode root = null;
        try {
            root = objectMapper.readTree(requestBody);
        }catch (Exception e){
            throw new BadCredentialsException(e.getLocalizedMessage());
        }
        if(root.isEmpty()) {
            throw new BadCredentialsException("no authentication factor data found.");
        }
        List<Authentication> authentications = new ArrayList<>();
        for(JsonNode node : root) {
            String type = node.get(LambdaUtil.getFieldName(AuthenticationFactor::getType)).asText();
            Class<AuthenticationFactorConvertor> convertorClz = (Class<AuthenticationFactorConvertor>) serverSecurityProperties.getAuthenticationFactorConvertors().get(type);
            Assert.notNull(convertorClz, "authentication factor convertor type:{} not config", type);
            AuthenticationFactorConvertor authenticationFactorConvertor = SpringUtil.getBean(convertorClz);
            Object authenticationFactor = null;
            try {
                authenticationFactor = objectMapper.treeToValue(node, authenticationFactorConvertor.getFactorType());
            }catch (Exception e){
                throw new BadCredentialsException(e.getLocalizedMessage());
            }
            authentications.add(authenticationFactorConvertor.convert(authenticationFactor));
        }

        return authentications;
    }
}

